package com.sxt.netty;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;

import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.ReferenceCountUtil;

import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @模块目的/功能：Netty编解码处理 
 * @版权说明： 
 * @版本号：1.0 
 * @日期：2018.11.13
 * @作者：罗林
 */
public class MsgCoderHandler extends ChannelInitializer<SocketChannel> {

	@Override
	protected void initChannel(SocketChannel sc) throws Exception {
		//添加编解码
		ChannelPipeline pipeline = sc.pipeline();

		// 超过15分钟未收到客户端消息则自动断开客户端连接
		pipeline.addLast("idleStateHandler", new IdleStateHandler(15, 0, 0, TimeUnit.MINUTES));

		/**
		 * LineBasedFrameDecoder: 使用行尾控制字符（\n或者\r\n）作为分隔符来解析数据。
		   *                                                                通过LineBasedFrameDecoder和StringDecoder来解决以回车换行符作为消息结束符的TCP黏包的问题；
		 * 
		 * DelimiterBasedFrameDecoder: 使用自定义的特殊字符作为消息的分隔符。
		 * 
		 * FixedLengthFrameDecoder: 是固定长度解码器，它能按照指定的长度对消息进行自动解码，开发者不需要考虑TCP的粘包等问题。
		 * 						          利用FixedLengthFrameDecoder解码，无论一次性接收到多少的数据，他都会按照构造函数中设置的长度进行解码；
		  *                                                                      如果是半包消息，FixedLengthFrameDecoder会缓存半包消息并等待下一个包，到达后进行拼包，直到读取完整的包。
		 * 
		 * LengthFieldBasedFrameDecoder: 自定义长度解码器解决TCP黏包问题。
		 * 
		 * HttpObjectDecoder: HTTP数据的解码器
		 * 
		 * 
		 */
		// 基于换行符号,1024表示单条消息的最大长度,解码器在查找分隔符时,达到该长度还没找到的话则抛出异常
		pipeline.addLast("framer", new LineBasedFrameDecoder(1024));
		// String解码器将收到对象自动转换成字符串对象
		pipeline.addLast("decoder", new StringDecoder(Charset.forName("UTF-8"))); // Netty4自带的String解码器 Charset.forName("UTF-8")
		pipeline.addLast("encoder", new StringEncoder(Charset.forName("UTF-8"))); // Netty4自带的String编码器Charset.forName("GBK")
		pipeline.addLast(new ByteStreamHandler()); // 自己实现的处理器
	}

	/**
	 * @模块目的/功能：业务逻辑处理 
	 * @版权说明： 
	 * @版本号：1.0 
	 * @日期：2018.11.13
	 * @作者：罗林
	 */
	private class ByteStreamHandler extends ChannelInboundHandlerAdapter {
		
		private int UNCONNECT_NUM = 0;
		
		/**
		 * @函数的名称:channelRead
		 * @目的/功能:从通道中读取数据
		 * @输入参数:ChannelHandlerContext 通道，Object 客户端发来的数据
		 * @返回值:无
		 * @throws Exception
		 * @作者:罗林
		 * @日期:2018.11.13
		 */
		@Override
		public void channelRead(ChannelHandlerContext ctx, Object msg) {
			try {
              //通知您已经链接上客户端
              String str = "";
				
				// 如果采用String解码器
				String msgStr = (String) msg;
				// 判断接收信息是否为空
				if (msgStr == null || msgStr.length() <= 0) {
					// donothing
					str = "接收信息为空！";
				} else {
					// donothing
					str = "服务端收到：" + new Date() + " " + msgStr;
				}
				System.out.println(str);
//
//                //通知您已经链接上客户端
//                String str = "服务端收到：" + new Date() + " " + msgStr;
                ByteBuf buf = Unpooled.buffer(str.getBytes().length);
                buf.writeBytes(str.getBytes("UTF-8"));
                ctx.writeAndFlush(buf);
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			} finally {
				// 释放资源
				release(msg);
			}
		}

		/**
		 * @函数的名称:exceptionCaught
		 * @目的/功能:发生异常时，处理函数
		 * @输入参数:ChannelHandlerContext 通道，cause 异常信息
		 * @返回值:无
		 * @作者:罗林
		 * @日期:2018.11.13
		 */
		@Override
		public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
			ctx.close();
			System.out.println("发生异常:{" + cause.getMessage() + "}");
			// 出现异常时，可以发送或者记录相关日志信息，之后，直接断开该链接，并重新登录请求，建立通道
			// cause.printStackTrace();
		}

		/**
		 *      客户端连接会触发
		 * @函数的名称:channelActive
		 * @目的/功能:当客户端主动链接服务端的链接后，这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
		 * @输入参数:ChannelHandlerContext 通道
		 * @返回值:无
		 * @作者:罗林
		 * @日期:2018.11.13
		 */
		@Override
		public void channelActive(ChannelHandlerContext ctx) throws Exception {
			// 通知您已经链接上客户端
			String str = "您已经开启与服务端链接" + " " + new Date() + " " + ctx.channel().localAddress();
			ByteBuf buf = Unpooled.buffer(str.getBytes().length);
			buf.writeBytes(str.getBytes("UTF-8"));
			ctx.writeAndFlush(buf);
		}

		/**
		 * @函数的名称:channelActive
		 * @目的/功能:当客户端主动断开服务端的链接后，这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
		 * @输入参数:ChannelHandlerContext 通道
		 * @返回值:无
		 * @作者:罗林
		 * @日期:2018.11.13
		 */
		@Override
		public void channelInactive(ChannelHandlerContext ctx) throws Exception {
			System.out.println(ctx.channel().localAddress().toString() + " channelInactive");
		}

		/**
		 * @函数的名称:userEventTriggered
		 * @目的/功能:用户自定义触发事件
		 * @输入参数:ChannelHandlerContext 通道
		 * @返回值:无
		 * @作者:罗林
		 * @日期:2018.11.13
		 */
		@Override
		public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
			if (IdleStateEvent.class.isAssignableFrom(evt.getClass())) {
				if (UNCONNECT_NUM >= 4) {
					System.err.println("client connect status is disconnect.");
					ctx.close();
					// 此处当重启次数达到4次之后，关闭此链接后，清除服务端相关的记录信息
					return;
				}
				IdleStateEvent event = (IdleStateEvent) evt;
				// 超时关闭channel
				switch (event.state()) {
				case WRITER_IDLE:
					System.out.println("send ping to client---date=" + new Date());
					// 发送心跳
					ctx.writeAndFlush("ping\n");
					UNCONNECT_NUM++;
					System.err.println("writer_idle over. and UNCONNECT_NUM=" + UNCONNECT_NUM);
					break;
				case READER_IDLE:
					// 读取服务端消息超时时，直接断开该链接，并重新登录请求，建立通道
					System.err.println("reader_idle over.");

				case ALL_IDLE:
					// 读取服务端消息超时时，直接断开该链接，并重新登录请求，建立通道
					System.err.println("all_idle over.");

				default:
					break;
				}
			}
		}

		/*
		 * channelReadComplete
		 * 
		 * channel 通道 Read 读取 Complete 完成
		 * 
		 * 在通道读取完成后会在这个方法里通知，对应可以做刷新操作 ctx.flush()
		 * 
		 */
		@Override
		public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//            System.out.println(sb.toString());
			ctx.flush();
		}

		private void release(Object msg) {
			try {
				ReferenceCountUtil.release(msg);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}

	}

}